#include "qtdbusobjectmanager.h"
#include "private/qtdbusobjectmanager_p.h"

#include "qtsignalcatcher.h"
#include "qtdbusproperties.h"
#include "private/qtdbusnode_p.h"

#include <QMetaMethod>
#include <QDBusMessage>
#include <QDBusContext>
#include <QDBusMetaType>

#include <private/qobject_p.h>
#include <private/qdbuscontext_p.h>
#include <private/qdbusconnection_p.h>
#include <private/dbus_minimal_p.h>

#include <QDebug>

extern Q_DBUS_EXPORT QString qDBusGenerateMetaObjectXml(QString interface, const QMetaObject *mo, const QMetaObject *base, int flags);

template<class Object, class ObjectPrivate, class Member, Member m>
struct DPointerGetter
{
    friend ObjectPrivate *getDPointer(Object *o) { return (o->*m); }
    friend void setDPointer(Object *o, ObjectPrivate *op) { o->*m = op; }
};

template struct DPointerGetter<QDBusContext, QDBusContextPrivate, decltype(&QDBusContext::d_ptr), &QDBusContext::d_ptr>;

QDBusContextPrivate *getDPointer(QDBusContext *object);
void setDPointer(QDBusContext *object, QDBusContextPrivate *objectPrivate);

struct DBusContextStacker
{
    DBusContextStacker(QObject *object, QDBusContextPrivate *target);
    ~DBusContextStacker();

    QPointer<QObject> object;
    QDBusContextPrivate *previous;
};

DBusContextStacker::DBusContextStacker(QObject *object, QDBusContextPrivate *target) : object{object}
{
    if (QDBusContext * const context = reinterpret_cast<QDBusContext *>(object->qt_metacast("QDBusContext"))) {
        previous = getDPointer(context);
        setDPointer(context, target);
    }
}

DBusContextStacker::~DBusContextStacker()
{
    if (object.isNull())
        return;

    if (QDBusContext * const context = reinterpret_cast<QDBusContext *>(object->qt_metacast("QDBusContext")))
        setDPointer(context, previous);
}

static int registerMetaType()
{
    qDBusRegisterMetaType<QMap<QDBusObjectPath, QVariantMap>>();
    qDBusRegisterMetaType<QMap<QString, QVariantMap>>();
    qDBusRegisterMetaType<QMap<QDBusObjectPath, QMap<QString, QVariantMap>>>();
    return 0;
}

const QMetaMethod QtDBusObjectManagerPrivate::destroyedSignal = QMetaMethod::fromSignal(&QObject::destroyed);

QtDBusObjectManagerPrivate::QtDBusObjectManagerPrivate()
{
}

std::optional<QString> QtDBusObjectManagerPrivate::subPath(QString path) const
{
    if (!path.startsWith(prefix))
        return std::nullopt;

    path.remove(0, prefix.length());
    if (path.isEmpty())
        return path;
    if (path.startsWith('/') && option == QDBusConnection::SubPath)
        return path;
    return std::nullopt;
}

QDBusObjectPath QtDBusObjectManagerPrivate::objectPath(const QString &subPath) const
{
    QStringList nodes = prefix.split('/');
    nodes.append(subPath.split('/', Qt::SkipEmptyParts));
    return QDBusObjectPath{nodes.join('/')};
}

QtDBusObjectManager::QtDBusObjectManager(QObject *parent) : QDBusVirtualObject{parent}
    , data{new QtDBusObjectManagerPrivate{}}
{
    static int callonce = registerMetaType();

    data->catcher = new QtSignalCatcher{this};
    data->properties = new QtDBusProperties{this};
#if QT_VERSION_MAJOR < 6
    connect(data->catcher, &QtSignalCatcher::catched, this, &QtDBusObjectManager::catched);
#else
    connect(data->catcher, &QtSignalCatcher::catched, this, static_cast<void (QObject::*)(QObject *,const QMetaMethod &,void **)>(&QtDBusObjectManager::catched));
#endif
}

QtDBusObjectManager::~QtDBusObjectManager()
{
    unregisterPath();
}

QString QtDBusObjectManager::introspect(const QString &path) const
{
    const std::optional<QString> &subPath = data->subPath(path);
    if (subPath == std::nullopt)
        return {};

    if (QtDBusNode *node = data->child(subPath->split('/', Qt::SkipEmptyParts)))
        return node->introspect();
    return {};
}

bool QtDBusObjectManager::handleMessage(const QDBusMessage &message, const QDBusConnection &connection)
{
    if (message.type() != QDBusMessage::MethodCallMessage)
        return false;

    const std::optional<QString> &path = data->subPath(message.path());
    if (path == std::nullopt)
        return false;

    const QString &interface = message.interface();
    if (interface == QStringLiteral(DBUS_INTERFACE_INTROSPECTABLE))
        return false;

    QObject *object = data->objectAt(path->split('/', Qt::SkipEmptyParts), message.interface());
    if (!object && interface == QStringLiteral(DBUS_INTERFACE_PROPERTIES))
        object = data->properties;

    if (!object) {
        qDebug() << "Message from unregistered interfaces:" << message;
        return false;
    }

#if QT_VERSION_MAJOR < 6
    QMetaObject::invokeMethod(this, std::bind(&QtDBusObjectManager::route, this, object, message), Qt::QueuedConnection);
#else
    QMetaObject::invokeMethod(this, static_cast<void (QObject::*)(QObject *, const QDBusMessage &)>(&QtDBusObjectManager::route), Qt::QueuedConnection, object, message);
#endif
    return true;
}

QDBusConnection QtDBusObjectManager::connection() const
{
    if (data->connection == std::nullopt)
        return QDBusConnectionPrivate::q(nullptr);
    return data->connection.value();
}

QString QtDBusObjectManager::path() const
{
    return data->prefix;
}

QDBusConnection::VirtualObjectRegisterOption QtDBusObjectManager::option() const
{
    return data->option;
}

bool QtDBusObjectManager::isRegistered() const
{
    return data->connection.has_value() && data->connection->isConnected();
}

bool QtDBusObjectManager::registerPath(const QDBusConnection &connection, const QString &path, QDBusConnection::VirtualObjectRegisterOption option)
{
    if (isRegistered() || !QDBusConnection{connection}.registerVirtualObject(path, this, option))
        return false;

    data->connection = connection;
    data->prefix = path;
    data->option = option;
    registerObject({}, {}, this);
    return true;
}

void QtDBusObjectManager::unregisterPath()
{
    const QtDBusNode::LocationList &locations = data->interfaces();
    for (const QtDBusNode::Location &location : locations) {
        const QString interface = location.interface->name;
        const QString &subPath = location.nodes.join('/');
        unregisterObject(subPath, interface);
    }
    data->connection.reset();
    data->prefix.clear();
}

bool QtDBusObjectManager::registerObject(const QString &subPath, const QString &interface, QObject *object)
{
    if (!subPath.isEmpty() && data->option != QDBusConnection::SubPath || !isRegistered())
        return false;

    const QtDBusNode::Location &location = data->registerObject(object, subPath.split('/', Qt::SkipEmptyParts), interface);
    if (!location.interface)
        return false;

    data->catcher->catchObject(object);

    QMap<QString, QVariantMap> interfaces;
    interfaces[location.interface->name] = QtDBusProperties::propertiesOf(location.interface->object);
    Q_EMIT InterfacesAdded(data->objectPath(subPath), interfaces);
    return true;
}

void QtDBusObjectManager::unregisterObject(const QString &subPath, const QString &interface)
{
    if (QObject *object = data->unregisterObject(subPath.split('/', Qt::SkipEmptyParts), interface)) {
        if (data->exist(object))
            return;
        data->catcher->releaseObject(object);
        Q_EMIT InterfacesRemoved(data->objectPath(subPath), {interface});
    }
}

void QtDBusObjectManager::unregisterObject(QObject *object)
{
    const QtDBusNode::LocationList &locations = data->locationsOf(object, {});
    for (const QtDBusNode::Location &location : locations) {
        const QString interface = location.interface->name;
        const QString &subPath = location.nodes.join('/');
        unregisterObject(subPath, interface);
    }
}

QObject *QtDBusObjectManager::objectAt(const QString &path, const QString &interface) const
{
    const std::optional<QString> &subPath = data->subPath(path);
    if (subPath == std::nullopt)
        return nullptr;

    return data->objectAt(subPath->split('/', Qt::SkipEmptyParts), interface);
}

void QtDBusObjectManager::catched(QObject *object, const QMetaMethod &signal, void **argv)
{
    if (signal == data->destroyedSignal)
        return unregisterObject(object);

    const QtDBusNode::LocationList &locations = data->locationsOf(object, {});
    if (locations.empty())
        return;

#if QT_VERSION_MAJOR < 6
    QVector<int> types;
#else
    QVector<QMetaType> types;
#endif
    QString error;
    qDBusParametersForMethod(signal.parameterTypes(), types, error);
    QVariantList args;
    const int size = types.size();
    args.reserve(size);
    for (int i = 1; i < size; ++i)
        args << QVariant{types.at(i), argv[i]};

    for (const QtDBusNode::Location &location : locations) {
        const QString &interface = location.interface->name;
        const QString &path = data->prefix + location.nodes.join('/');
        QDBusMessage message = QDBusMessage::createSignal(data->prefix, interface, signal.name());
        message.setArguments(args);
        connection().send(message);
    }
}

void QtDBusObjectManager::route(QObject *object, const QDBusMessage &message)
{
    const QString &path = message.path();
    const QString &interface = message.interface();
    const QString &member = message.member();
    QVariantList &&arguments = message.arguments();
    const int argumentCount = arguments.size();

    QVarLengthArray<void *, 10> variants;
    variants.append(nullptr);
    QStringList signatures;
    for (QVariant &var : arguments) {
        signatures.append(var.typeName());
        variants.append(var.data());
    }

    const QMetaObject *metaObject = object->metaObject();
    const QByteArray slot = QStringLiteral("%1(%2)").arg(member, signatures.join(',')).toUtf8();
    const int slotIndex = metaObject->indexOfSlot(slot);
    if (slotIndex == -1)
        return;

    const QMetaMethod &method = metaObject->method(slotIndex);
    QVariantList returnVariant;
#if QT_VERSION_MAJOR < 6
    if (const int returnType = method.returnType();
        (returnType != QMetaType::Void) && (returnType != QMetaType::UnknownType)) {
        returnVariant.append(QVariant{returnType, QMetaType{returnType}.create()});
#else
    if (const QMetaType &returnType = method.returnMetaType();
        (returnType.id() != QMetaType::Void) && (returnType.id() != QMetaType::UnknownType)) {
        returnVariant.append(QVariant{returnType, returnType.create()});
#endif
        variants[0] = returnVariant[0].data();
    }

    {
        QDBusContextPrivate context{connection(), message};
        DBusContextStacker stacker{object, &context};
        object->qt_metacall(QMetaObject::InvokeMetaMethod, slotIndex, variants.data());
    }

    if (message.isReplyRequired() && !message.isDelayedReply()) {
        const QDBusMessage &reply = message.createReply(returnVariant);
        connection().send(reply);
    }
}

QMap<QDBusObjectPath, QMap<QString, QVariantMap>> QtDBusObjectManager::GetManagedObjects() const
{
    QMap<QDBusObjectPath, QMap<QString, QVariantMap>> result;
    for (QtDBusNode::Location &location : data->interfaces()) {
        result[QDBusObjectPath{data->prefix + location.nodes.join('/')}][location.interface->name] =
            QtDBusProperties::propertiesOf(location.interface->object);
    }
    return result;
}
